﻿using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Lua.Converters;

namespace System.Lua
{
    /// <summary>
    /// Represents a writer that provides a fast, non-cached, forward only means of generating streams
    /// or files containing Lua code.
    /// </summary>
    public sealed class LuaWriter : IDisposable
    {
        private const string nilExpression = "nil";

        private const string longBracketStringLiteralFormat = "[[{0}]]";
        private const string doubleQuoteStringLiteralFormat = "\"{0}\"";
        private const string longBracketStringLiteralClose = "]]";
        private const string closingBracket = "]";
        private const char space = ' ';

        private static readonly string[] NeedsLongBracket = new[]
                                                            {
                                                                "\r",
                                                                "\n",
                                                                "\"",
                                                                "\\"
                                                            };

        private static readonly Type[] LiteralTypes = new[]
                                                      {
                                                          typeof(Byte),
                                                          typeof(Int16), 
                                                          typeof(Int32), 
                                                          typeof(Int64), 
                                                          typeof(Single),
                                                          typeof(Double)
                                                      };

        private TextWriter textWriter;

        private LuaWriter(TextWriter textWriter)
        {
            if (textWriter == null)
                throw new ArgumentNullException("textWriter");

            this.textWriter = textWriter;
        }

        /// <summary>
        /// Creates a new <see cref="LuaWriter"/>.
        /// </summary>
        /// <param name="textWriter">A <see cref="textWriter"/> to use.</param>
        /// <returns>An instance of the <see cref="LuaWriter"/> class.</returns>
        public static LuaWriter Create(TextWriter textWriter)
        {
            if (textWriter == null)
                throw new ArgumentNullException("textWriter");

            return new LuaWriter(textWriter);
        }

        /// <summary>
        /// Write the start of a Lua assignment.
        /// </summary>
        /// <param name="variableName">The name of the variable that is assigned to.</param>
        public void WriteStartAssignment(string variableName)
        {
            if (variableName == null)
                throw new ArgumentNullException("variableName");

            this.Write(String.Format(CultureInfo.InvariantCulture, "{0} = ", variableName));
        }

        /// <summary>
        /// Write a literal expression.
        /// </summary>
        /// <param name="value">The literal value.</param>
        public void WriteLiteralExpression(object value)
        {
            // Handle null value
            if (value == null)
            {
                this.Write(nilExpression);
                return;
            }

            // Get the type of the value
            Type type = value.GetType();

            // Check whether the value should be written out as it is
            if (LiteralTypes.Contains(type))
            {
                // Write the literal value
                this.Write(value);
                return;
            }

            // Write literal as string using type converter
            ILuaTypeConverter converter = LuaTypeDescriptor.GetConverter(value);
            if (converter != null)
            {
                string stringValue = converter.ConvertToString(value);

                // Check whether the converter used is a custom Lua converter that can return Lua literals
                if (converter.ReturnsLiteral)
                    this.Write(stringValue);
                else
                    this.WriteStringLiteralExpression(stringValue);

                return;
            }

            throw new ArgumentException(Strings.ValueTypeConverterMissing, "value");
        }

        private void WriteStringLiteralExpression(string value)
        {
            // Determine whether the string literal value contains any substring that requires the long bracket format
            bool useLongBracket = value.ContainsAny(NeedsLongBracket);

            // Check whether when using the long bracket format, the string literal would try to close the long bracket
            while (useLongBracket && value.Contains(longBracketStringLiteralClose))
            {
                // Remove the offending part from the value with an escaped version
                value = value.Replace(longBracketStringLiteralClose, String.Empty);
            }

            // Check whether when using the long bracket format, the string literal does not end with a closing bracket
            while (useLongBracket && value.EndsWith(closingBracket, StringComparison.OrdinalIgnoreCase))
            {
                // Append a space to the value
                value = String.Concat(value, space);
            }

            // Write the string literal using the correct string literal format
            this.Write(String.Format(CultureInfo.InvariantCulture, useLongBracket ? longBracketStringLiteralFormat : doubleQuoteStringLiteralFormat, value));
        }

        /// <summary>
        /// Writes the end of a Lua assignment.
        /// </summary>
        public void WriteEndAssignment()
        {
            this.WriteLine();
        }

        /// <summary>
        /// Writes the start of a Lua table.
        /// </summary>
        public void WriteStartTable()
        {
            this.Write("{ ");
        }

        /// <summary>
        /// Writes the start of a Lua table field.
        /// </summary>
        public void WriteStartTableField()
        {
            this.WriteStartTableField(null);
        }

        /// <summary>
        /// Writes the start of a Lua table field.
        /// </summary>
        /// <param name="fieldName">The name of the field.</param>
        public void WriteStartTableField(object fieldName)
        {
            if (fieldName == null)
                return;
            
            Type fieldNameType = fieldName.GetType();
            if (fieldNameType != typeof(int) && fieldNameType != typeof(string))
                throw new ArgumentException(Strings.OnlyFieldExpressionAndFieldNamesAllowed, "fieldName");

            if (fieldNameType == typeof(int))
                this.Write(String.Format(CultureInfo.InvariantCulture, "[{0}] = ", fieldName));
            if (fieldNameType == typeof(string))
                this.Write(String.Format(CultureInfo.InvariantCulture, "{0} = ", fieldName));
        }

        /// <summary>
        /// Writes the end of a Lua table field.
        /// </summary>
        public void WriteEndTableField()
        {
            this.Write(", ");
        }

        /// <summary>
        /// Writes the end of a Lua table.
        /// </summary>
        public void WriteEndTable()
        {
            this.Write("}");
        }

        private void Write(object value)
        {
            Debug.Write(value);
            textWriter.Write(value);
        }

        private void WriteLine()
        {
            Debug.WriteLine(String.Empty);
            textWriter.WriteLine();
        }

        /// <summary>
        /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
        /// </summary>
        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        private void Dispose(bool disposing)
        {
            if (disposing)
            {
                // Dispose the underlying TextWriter
                if (textWriter != null)
                {
                    textWriter.Dispose();
                    textWriter = null;
                }
            }
        }
    }
}
